import * as buffer from "buffer";

export const PNG_MAGIC = '89504e470d0a1a0a'
export const ICO_MAGIC = '28000000'

export enum EnumSectionHeaderType {
  TEXT = '.text',
  ITEXT = '.itext',
  DATA = '.data	',
  BSS = '.bss	',
  IDATA = '.idata',
  DIDATA = '.didata',
  EIDATA = '.edata',
  RSRC = '.rsrc',
  RELOC = '.reloc',
  EDATA = '.edata',
  TLS = '.tls',
  RDATA = '.rdata',
}

export enum EnumResourceType {
  // 常见的资源类型及其ID
  RT_CURSOR = 1, // 光标资源
  RT_BITMAP = 2, // 位图资源
  RT_ICON = 3, // 图标资源
  RT_MENU = 4, // 菜单资源
  RT_DIALOG = 5, // 对话框资源
  RT_STRING = 6, // 字符串表资源
  RT_FONTDIR = 7, // 字体目录资源
  RT_FONT = 8, // 字体资源
  RT_ACCELERATOR = 9, // 快捷键表资源
  RT_RCDATA = 10, // 自定义数据资源
  RT_MESSAGETABLE = 11, // 消息表资源
  RT_GROUP_CURSOR = 12, // 组合光标资源（多个光标集合）
  RT_GROUP_ICON = 14, // 组合图标资源（多个图标集合）
  RT_VERSION = 16, // 版本信息资源
  RT_DLGINCLUDE = 17, // 包含对话框模板的资源
  RT_PLUGPLAY = 19, // 插件设备相关的资源
  RT_VXD = 20, // 虚拟设备驱动程序资源
  RT_ANICURSOR = 21, // 动画光标资源
  RT_ANIICON = 22, // 动画图标资源
  RT_HTML = 23, // HTML资源（HTML帮助文件）
  RT_MANIFEST = 24, // 应用程序清单资源（Windows XP SP2及更高版本）
  // 其他可能存在的非标准或不常用的资源类型
  // ...
}

export enum IconType {
  BMP = 1,
  ICO = 2,
  JPG = 3,
  PNG = 4,
  GIF = 5,
  TIFF = 6,
}

class SectionHeader {
  offset: number
  name: string
  virtualAddress: number
  sizeOfRawData: number
  pointerToRawData: number
  pointerToRelocations: number
  pointerToLinenumbers: number
  numberOfRelocations: number
  numberOfLinenumbers: number
  characteristics: number

  constructor(dataView: DataView, offset: number) {
    this.offset = offset
    const nameBytes: number[] = []
    for (let j = 0; j < 8; j++) {
      let nameOffset = this.offset + j
      const byte = dataView.getUint8(nameOffset)
      if (byte !== 0) {
        nameBytes.push(byte)
      }
    }
    this.name = String.fromCharCode(...new Uint8Array(nameBytes))
    this.virtualAddress = dataView.getUint32(offset + 12, true)
    this.sizeOfRawData = dataView.getUint32(offset + 16, true)
    this.pointerToRawData = dataView.getUint32(offset + 20, true)
    this.pointerToRelocations = dataView.getUint32(offset + 24, true)
    this.pointerToLinenumbers = dataView.getUint32(offset + 28, true)
    this.numberOfRelocations = dataView.getUint16(offset + 32, true)
    this.numberOfLinenumbers = dataView.getUint16(offset + 34, true)
    this.characteristics = dataView.getUint32(offset + 36, true)
  }
}

export class Version {
  productVersion?: string
}

class ResourceDirectoryTable {
  characteristics: number
  timeDateStamp: number
  majorVersion: number
  minorVersion: number
  numberOfNamedEntries: number
  numberOfIdEntries: number
  entries: ResourceDirectoryEntry[] = []

  constructor(dataView: DataView, offset: number, baseOffset?: number) {
    this.characteristics = dataView.getUint32(offset, true)
    this.timeDateStamp = dataView.getUint32(offset + 4, true)
    this.majorVersion = dataView.getUint16(offset + 8, true)
    this.minorVersion = dataView.getUint16(offset + 10, true)
    this.numberOfNamedEntries = dataView.getUint16(offset + 12, true)
    this.numberOfIdEntries = dataView.getUint16(offset + 14, true)
    const entriesStart = offset + 16
    for (
      let i = 0;
      i < this.numberOfIdEntries + this.numberOfNamedEntries;
      i++
    ) {
      this.entries.push(
        new ResourceDirectoryEntry(
          dataView,
          entriesStart + i * 8,
          baseOffset || offset
        )
      )
    }
  }
}

class ResourceDirectoryEntry {
  id: number
  offset: number
  subdirectoryOffset: number
  isLeaf: number
  resourceDirectoryTable: ResourceDirectoryTable | null = null
  resourceDataEntry: ResourceDataEntry | null = null

  constructor(dataView: DataView, offset: number, baseOffset: number) {
    this.offset = offset
    this.id = dataView.getUint32(offset, true)
    this.subdirectoryOffset = dataView.getUint16(offset + 4, true)
    this.isLeaf = dataView.getUint8(offset + 7)
    if (this.isLeaf == 0) {
      this.resourceDataEntry = new ResourceDataEntry(
        dataView,
        baseOffset + this.subdirectoryOffset
      )
      return
    }
    this.resourceDirectoryTable = new ResourceDirectoryTable(
      dataView,
      baseOffset + this.subdirectoryOffset,
      baseOffset
    )
  }
}

class ResourceDataEntry {
  offset: number
  dataSize: number
  codePage: number
  dataRva: number
  reserved: number = 0

  constructor(dataView: DataView, offset: number) {
    this.offset = offset
    this.dataRva = dataView.getUint32(offset, true)
    this.dataSize = dataView.getUint32(offset + 4, true)
    this.codePage = dataView.getUint32(offset + 8, true)
    this.reserved = dataView.getUint32(offset + 12, true)
  }
}

export class IconGroup {
  idReserved: number // 必须为0
  idType: number // 必须为1，表示ICO文件
  idCount: number // 图标数量
  entries: IconGroupEntry[] = []

  constructor(dataView: DataView, offset: number) {
    this.idReserved = dataView.getUint16(offset, true)
    this.idType = dataView.getUint16(offset + 2, true)
    this.idCount = dataView.getUint16(offset + 4, true)
    for (let i = 0; i < this.idCount; i++) {
      this.entries.push(new IconGroupEntry(dataView, offset + 6 + i * 14))
    }
  }

  public getData(): Uint8Array {
    const dataView = new DataView(new ArrayBuffer(6))
    dataView.setUint16(0, this.idReserved, true)
    dataView.setUint16(2, this.idType, true)
    dataView.setUint16(4, 1, true)
    return new Uint8Array(dataView.buffer)
  }
}

export class IconGroupEntry {
  width: number
  height: number
  colorCount: number = 16
  reserved: number = 0
  planes: number
  bitCount: number
  dwBytesInRes: number
  dwImageOffset: number = 22
  id?: number

  constructor(dataView: DataView, offset?: number) {
    if (!offset) {
      this.width = dataView.getUint32(4, true)
      this.height = dataView.getUint32(8, true)
      this.planes = dataView.getUint16(10, true)
      this.bitCount = dataView.getUint16(12, true)
      this.dwBytesInRes = dataView.buffer.byteLength
      return
    }
    this.width = dataView.getUint8(offset)
    this.height = dataView.getUint8(offset + 1)
    this.colorCount = dataView.getUint8(offset + 2)
    this.reserved = dataView.getUint8(offset + 3)
    this.planes = dataView.getUint16(offset + 4, true)
    this.bitCount = dataView.getUint16(offset + 6, true)
    this.dwBytesInRes = dataView.getUint32(offset + 8, true)
    this.id = dataView.getUint16(offset + 12, true)
  }

  public getData(): Uint8Array {
    const dataView = new DataView(new ArrayBuffer(16))
    dataView.setUint8(0, this.width)
    dataView.setUint8(1, this.height)
    dataView.setUint8(2, this.colorCount)
    dataView.setUint8(3, this.reserved)
    dataView.setUint16(4, this.planes, true)
    dataView.setUint16(6, this.bitCount, true)
    dataView.setUint32(8, this.dwBytesInRes, true)
    dataView.setUint32(12, this.dwImageOffset, true)
    return new Uint8Array(dataView.buffer)
  }
}

export class IconEntry {
  width: number
  height: number
  type?: number
  data: Uint8Array

  constructor(
    array: Uint8Array,
    iconGroup: IconGroup,
    iconGroupEntry?: IconGroupEntry
  ) {
    const magic: string = [...array.slice(0, 8)]
      .map((item: number) => {
        const str = item.toString(16)
        if (str.length < 2) {
          return '0' + str
        }
        return str + ''
      })
      .join('')
    if (PNG_MAGIC === magic) {
      this.width = 256
      this.height = 256
      this.type = IconType.PNG
      this.data = array
      return
    }

    if (!iconGroupEntry) {
      iconGroupEntry = new IconGroupEntry(new DataView(array.buffer))
    }

    this.width = iconGroupEntry.width
    this.height = iconGroupEntry.height
    this.type = IconType.ICO
    this.data = new Uint8Array([
      ...iconGroup.getData(),
      ...iconGroupEntry.getData(),
      ...array,
    ])
  }
}

/**
 * 从DataView中读取字符串
 * @param dataView DataView对象，包含需要读取的数据
 * @param options 选项对象，可包含offset属性来指定读取的起始位置
 * @returns 返回解析出的字符串
 */
const readString = (dataView: DataView, options: { offset?: number }) => {
  if (!options.offset) {
    options.offset = 0 // 如果没有指定起始偏移量，则默认为0
  }
  const charCode = dataView.getUint16(options.offset, true)
  let str = String.fromCharCode(charCode)
  if (str == null || str.charAt(0) == '\u0000') {
    return ''
  }
  options.offset += 2
  str = str + readString(dataView, options)
  return str
}
/**
 * 计算给定偏移量所需的填充字节数量。
 * 该方法主要用于确保数据在存储或处理时按照4字节边界对齐。
 *
 * @param offset 原始偏移量，必须为非负数。
 * @return 返回所需的填充字节数量。如果偏移量已经4字节对齐，则返回0；
 * 否则返回最小的字节数，使得添加该数量的字节后偏移量成为4的倍数。
 * @throws IllegalArgumentException 如果偏移量为负数，抛出此异常。
 */
const calculatePadding = (offset: number) => {
  // 检查偏移量是否为负数
  if (offset < 0) {
    throw new Error('Offset cannot be negative.')
  }
  const remainder = offset % 4 // 计算偏移量对4取余，以确定是否需要填充
  // 返回所需的填充字节数量
  return remainder == 0 ? 4 : (4 - remainder) % 4
}

export class VersionInfo {
  public wLength: number
  public wValueLength: number
  public wType: number
  public fixedFileInfo: FixedFileInfo
  public stringFileInfo: StringFileInfo
  public stringTable: StringTable
  public records: Record<string, StringTableRecord> = {}
  public static readonly LENGTH: number = 36

  constructor(dataView: DataView, offset?: number) {
    offset = offset || 0 // 假设offset是从资源表中获得的VS_VERSION_INFO起始地址
    //获取VersionInfo结构的长度
    this.wLength = dataView.getUint16(offset, true)
    this.wValueLength = dataView.getUint16(offset + 2, true)
    this.wType = dataView.getUint16(offset + 4, true)
    // 读取标识符字符串

    let currentOffset = offset + 6
    let identifier = readString(dataView, {offset: currentOffset})
    // 如果不是'VS_VERSION_INFO'，则可能是不正确的数据
    if (identifier !== 'VS_VERSION_INFO') {
      throw new Error('Invalid or missing VS_VERSION_INFO identifier')
    }
    currentOffset = offset + VersionInfo.LENGTH
    currentOffset += calculatePadding(currentOffset)
    this.fixedFileInfo = new FixedFileInfo(dataView, currentOffset)

    currentOffset = currentOffset + this.wValueLength
    this.stringFileInfo = new StringFileInfo(dataView, currentOffset)
    currentOffset = currentOffset + StringFileInfo.LENGTH
    currentOffset += calculatePadding(currentOffset)
    this.stringTable = new StringTable(dataView, currentOffset)
    const endOffset = currentOffset + this.stringTable.wLength
    currentOffset = currentOffset + StringTable.LENGTH
    currentOffset += calculatePadding(currentOffset)
    while (currentOffset < endOffset) {
      const record = new StringTableRecord(dataView, currentOffset)
      this.records[record.key] = record
      currentOffset = record.getEndOffset()
    }
  }
}

export class FixedFileInfo {
  dwSignature: number /* e.g. 0xfeef04bd */
  dwStructVersion: number /* e.g. 0x00000042 = "0.42" */
  dwFileVersionMS: string /* e.g. 0x00030075 = "3.75" */
  dwFileVersionLS: string /* e.g. 0x00000031 = "0.31" */
  dwProductVersionMS: string /* e.g. 0x00030010 = "3.10" */
  dwProductVersionLS: string /* e.g. 0x00000031 = "0.31" */
  dwFileFlagsMask: number /* = 0x3F for version "0.42" */
  dwFileFlags: number /* e.g. VFF_DEBUG | VFF_PRERELEASE */
  dwFileOS: number /* e.g. VOS_DOS_WINDOWS16 */
  dwFileType: number /* e.g. VFT_DRIVER */
  dwFileSubtype: number /* e.g. VFT2_DRV_KEYBOARD */
  dwFileDateMS: number /* e.g. 0 */
  dwFileDateLS: number /* e.g. 0 */
  constructor(dataView: DataView, offset?: number) {
    offset = offset || 0
    this.dwSignature = dataView.getUint32(offset, true)
    this.dwStructVersion = dataView.getUint32(offset + 4, true)
    this.dwFileVersionMS =
      dataView.getUint16(offset + 8 + 2, true) +
      '.' +
      dataView.getUint16(offset + 8, true)
    this.dwFileVersionLS =
      dataView.getUint16(offset + 12 + 2, true) +
      '.' +
      dataView.getUint16(offset + 12, true)
    this.dwProductVersionMS =
      dataView.getUint16(offset + 16 + 2, true) +
      '.' +
      dataView.getUint16(offset + 16, true)
    this.dwProductVersionLS =
      dataView.getUint16(offset + 20 + 2, true) +
      '.' +
      dataView.getUint16(offset + 20, true)
    this.dwFileFlagsMask = dataView.getUint32(offset + 24, true)
    this.dwFileFlags = dataView.getUint32(offset + 28, true)
    this.dwFileOS = dataView.getUint32(offset + 32, true)
    this.dwFileType = dataView.getUint32(offset + 36, true)
    this.dwFileSubtype = dataView.getUint32(offset + 40, true)
    this.dwFileDateMS = dataView.getUint32(offset + 44, true)
    this.dwFileDateLS = dataView.getUint32(offset + 48, true)
  }
}

export class StringFileInfo {
  wLength: number
  wValueLength: number
  wType: number
  signature: string
  static readonly LENGTH: number = 34

  constructor(dataView: DataView, offset?: number) {
    offset = offset || 0
    this.wLength = dataView.getUint16(offset, true)
    this.wValueLength = dataView.getUint16(offset + 2, true)
    this.wType = dataView.getUint16(offset + 4, true)
    this.signature = readString(dataView, {offset: offset + 6})
    // 读取标识符字符串
  }
}

export class StringTable {
  wLength: number
  wValueLength: number
  wType: number
  signature: string
  static readonly LENGTH: number = 22

  constructor(dataView: DataView, offset?: number) {
    offset = offset || 0
    this.wLength = dataView.getUint16(offset, true)
    this.wValueLength = dataView.getUint16(offset + 2, true)
    this.wType = dataView.getUint16(offset + 4, true)
    this.signature = readString(dataView, {offset: offset + 6})
  }
}

export class StringTableRecord {
  offset: number
  wLength: number
  wValueLength: number
  wType: number
  key: string
  value: string
  padding: number

  constructor(dataView: DataView, offset?: number) {
    this.offset = offset || 0
    while (!dataView.getUint16(this.offset, true)) {
      this.offset += 2
    }
    this.wLength = dataView.getUint16(this.offset, true)
    this.wValueLength = dataView.getUint16(this.offset + 2, true)
    this.wType = dataView.getUint16(this.offset + 4, true)
    const options = {offset: this.offset + 6}
    this.key = (readString(dataView, options) || '').trim()
    options.offset += calculatePadding(options.offset)
    this.value = (readString(dataView, options) || '').trim()
    this.padding = calculatePadding(this.offset + this.wLength - 2)
  }

  getEndOffset() {
    return this.offset + this.wLength - 2 + this.padding
  }
}

export class PeFile {
  private file:File
  private dataView: DataView | null = null
  private static DOS_HEADER_OFFSET: number = 0
  private static DOS_HEADER_SIZE: number = 0x3c
  private peHeaderOffset: number = 0
  private optionalHeaderSize: number = 0
  private sectionNum: number = 0
  private sectionHeadersOffset: number = 0
  private sectionHeaders: Record<string, SectionHeader> = {}
  private resourceDirectoryTable?: ResourceDirectoryTable
  private initPromise: Promise<void>

  constructor(file: File) {
    const _this = this
    this.file = file
    this.initPromise = new Promise<void>(async (resolve) => {
      console.log('begin')
      await _this.initDataView()
      console.log('initDataView')
      _this.initPeHeaderOffset()
      console.log('initPeHeaderOffset')
      _this.initOptionalHeaderSize()
      console.log('initOptionalHeaderSize')
      _this.initSectionNum()
      console.log('initSectionNum')
      _this.initSectionHeadersOffset()
      console.log('initSectionHeadersOffset')
      _this.initSectionHeader()
      console.log('initSectionHeader')
      _this.initResource()
      console.log('initResource')
      resolve()
    })
  }

  private async initDataView() {
    if (this.file === null) {
      throw new Error('文件为空')
    }
    const buffer:ArrayBuffer = await this.file.slice(0, 0x3c).arrayBuffer()
    const dataView = new DataView(buffer)
    const magic = dataView.getUint16(0, true)
    if (magic !== 23117) {
      throw new Error('不是exe')
    }
  }

  private throwErrorIfFileEmpty(arrayBuffer: ArrayBuffer | null) {
    if (arrayBuffer == null) {
      throw new Error('文件为空')
    }
  }

  private initPeHeaderOffset() {
    // 获取PE头偏移量
    this.peHeaderOffset = this.dataView!.getUint32(
      PeFile.DOS_HEADER_OFFSET + PeFile.DOS_HEADER_SIZE,
      true
    )
  }

  private initOptionalHeaderSize() {
    this.optionalHeaderSize = this.dataView!.getUint16(
      this.peHeaderOffset + 20,
      true
    )
  }

  private initSectionNum() {
    this.sectionNum = this.dataView!.getUint16(this.peHeaderOffset + 6, true)
  }

  private initSectionHeadersOffset() {
    this.sectionHeadersOffset =
      this.peHeaderOffset + 20 + 4 + this.optionalHeaderSize
  }

  private initSectionHeader() {
    for (let i = 0; i < this.sectionNum; i++) {
      const sectionHeader = new SectionHeader(
        this.dataView!,
        this.sectionHeadersOffset + i * 40
      )
      this.sectionHeaders[sectionHeader.name] = sectionHeader
    }
  }

  private initResource() {
    this.resourceDirectoryTable = new ResourceDirectoryTable(
      this.dataView!,
      this.getResourceOffset()
    )
  }

  private getResourceOffset() {
    const sectionHeader = this.sectionHeaders[EnumSectionHeaderType.RSRC]
    if (sectionHeader === undefined) {
      throw new Error('没有找到资源文件')
    }
    return sectionHeader.pointerToRawData
  }

  public async getIconGroups() {
    const entries = await this.getResourceDataEntriesById(
      EnumResourceType.RT_GROUP_ICON
    )
    const _this = this
    const headers = entries.map((dataEntry) => {
      const offset =
        _this.sectionHeaders[EnumSectionHeaderType.RSRC].pointerToRawData +
        dataEntry.dataRva -
        _this.sectionHeaders[EnumSectionHeaderType.RSRC].virtualAddress
      return new IconGroup(_this.dataView!, offset)
    })
    return headers
  }

  public async getIcons() {
    const _this = this

    const iconEntries = await this.getResourceDataEntriesById(
      EnumResourceType.RT_ICON
    )

    const iconGroups = await this.getIconGroups()
    const iconGroupEntries: Record<number, IconGroupEntry> = []
    iconGroups.forEach((group) => {
      group.entries.forEach((item) => {
        iconGroupEntries[item.id! - 1] = item
      })
    })

    return iconEntries.map((dataEntry, index: number) => {
      const offset =
        _this.sectionHeaders[EnumSectionHeaderType.RSRC].pointerToRawData +
        dataEntry.dataRva -
        _this.sectionHeaders[EnumSectionHeaderType.RSRC].virtualAddress
      return new IconEntry(
        new Uint8Array(_this.dataView!.buffer, offset, dataEntry.dataSize),
        iconGroups[0],
        iconGroupEntries[index]
      )
    })
  }

  public async getVersionInfo() {
    const _this = this
    const entries = await this.getResourceDataEntriesById(
      EnumResourceType.RT_VERSION
    )
    const infos = entries.map((entry) => {
      const offset =
        _this.sectionHeaders[EnumSectionHeaderType.RSRC].pointerToRawData +
        entry.dataRva -
        _this.sectionHeaders[EnumSectionHeaderType.RSRC].virtualAddress
      return new VersionInfo(
        new DataView(_this.dataView!.buffer, offset, entry.dataSize)
      )
    })
    if (infos.length > 0) {
      return infos[0]
    } else {
      return null
    }
  }

  public async getResourceDataEntriesById(id: number) {
    await this.initPromise
    const entries = this.resourceDirectoryTable!.entries.filter(
      (entry) => entry.id == id
    )
    if (entries.length < 1) {
      throw new Error('没有找到信息:' + id)
    }
    return this.getResourceDataEntriesByEntry(entries[0])
  }

  getResourceDataEntriesByEntry(
    entry: ResourceDirectoryEntry
  ): ResourceDataEntry[] {
    if (entry.isLeaf == 0) {
      return (entry.resourceDataEntry && [entry.resourceDataEntry]) || []
    } else if (entry.resourceDirectoryTable) {
      const entries = entry.resourceDirectoryTable.entries
      const dataEntries: ResourceDataEntry[] = []
      entries.forEach((entry) => {
        dataEntries.push(...this.getResourceDataEntriesByEntry(entry))
      })
      return dataEntries
    } else {
      return []
    }
  }
}
